Executor.ts ➔ get   A
last analyzed

Complexity

Conditions 3

Size

Total Lines 13
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 13
ccs 7
cts 7
cp 1
rs 9.85
c 0
b 0
f 0
cc 3
crap 3
1 10
import EventEmitter from 'events';
2 10
import { logger } from '@/core';
3 10
import {
4
  isError, Result, wrap,
5
} from '@/core/errors';
6
7 10
const phases = {
8
  0: 'start',
9
  1: 'register',
10
  2: 'routing',
11
  3: 'init',
12
  4: 'exit',
13
} as const;
14
export type ExecutorPhase = typeof phases[keyof typeof phases];
15
16
type ExecutorEventMap = {
17
  [phase in ExecutorPhase]: [Result<void>[]];
18
} & {
19
  error: [Error];
20
  finished: [];
21
};
22
23
class ExecutorImpl {
24 10
  static readonly PHASES = phases;
25
26 9
  tasks: Map<ExecutorPhase, Array<() => Promise<void> | void>> = new Map();
27
28 9
  eventEmitter = new EventEmitter<ExecutorEventMap>();
29
30 9
  phasePromises: Map<ExecutorPhase, Promise<Result<void>[]>> = new Map();
31
32 9
  started = false;
33
34 9
  execution: Promise<Result<void>[]> | null = null;
35
36
37
38
  /**
39
 * Registers a task to be executed in a given phase.
40
 * @param task {() => Promise<void> | void} function to be executed, can return a Promise or void
41
 * @param phase {ExecutorPhase} phase in which the task should be executed
42
 */
43
  setExecution(phase: ExecutorPhase, task: () => Promise<void> | void) {
44 77
    let phaseTasks = this.tasks.get(phase) ?? [];
45 77
    phaseTasks.push(task);
46 77
    this.tasks.set(phase, phaseTasks);
47
  }
48
49
  getExecutionPhase(phase: ExecutorPhase) {
50 48
    return this.phasePromises.get(phase) ?? new Promise((resolve) => {
51 11
      this.eventEmitter.once(phase, resolve);
52
    });
53
  }
54
55
  /**
56
 * Executes all registered tasks in a defined sequence based on phases.
57
 * Tasks are sorted by their phase index and executed concurrently within each phase.
58
 * Debug logs are generated for each phase indicating the number of tasks being executed.
59
 */
60
  async execute(): Promise<Result<void, Error>[]> {
61 56
    this.started = true;
62 56
    return Object.entries(phases)
63 280
      .filter(([, phase]) => phase !== 'exit')
64 224
      .map(([idx, phase]) => [Number(idx), phase] as const)
65 168
      .sort(([a], [b]) => a - b)
66 224
      .map(([, phase]) => phase)
67
      .reduce((
68
        previous: {
69
          promise: Promise<Result<void>[]>, phase: ExecutorPhase
70
        },
71
        currentPhase,
72 224
      ) => ({
73
        promise: previous.promise.then(
74 224
          async () => {
75 224
            await previous.promise;
76 224
            return this.executePhase(currentPhase);
77
          },
78
        ),
79
        phase: currentPhase,
80
      }), { promise: Promise.resolve<Result<void, Error>[]>([]), phase: 'start' })
81
      .promise;
82
  }
83
84
  /**
85
   * Executes all tasks in a given phase
86
   * @param phase {ExecutorPhase}
87
   * @returns {Promise<Result<void>[]>}
88
   */
89
  private async executePhase(phase: ExecutorPhase) {
90 313
    const tasksToExecute = this.tasks.get(phase) ?? [];
91 313
    logger.debug(`Executing phase ${phase} with ${tasksToExecute.length} tasks`);
92 313
    const wrappedTasks = tasksToExecute.map(async (task) => {
93 84
      const result = wrap(task);
94 84
      return Promise.resolve(result);
95
    });
96
97 313
    const resultMap = async (result: Result<void>, index: number) => {
98 84
      if (isError(result)) {
99 6
        logger.error(`Task ${index} in phase ${phase} failed`, { cause: result.error });
100 6
        this.eventEmitter.emit('error', result.error);
101
      }
102 84
      return result;
103
    };
104
105 313
    const phasePromise = Promise.all(wrappedTasks)
106 313
      .then((results) => Promise.all(results.map(resultMap)));
107
108 313
    this.phasePromises.set(phase, phasePromise);
109 313
    return phasePromise
110
      .then((result) => {
111 313
        this.eventEmitter.emit(phase, result);
112 313
        return result;
113
      });;
114
  }
115
116
  private beforeExit() {
117 18
    this.stopLifecycle();
118
  }
119
120
  /**
121
 * Starts the lifecycle of the ExpressBeans application.
122
 * If there are tasks to execute, they are executed in the defined sequence.
123
 * If execution is already in progress, the function does nothing.
124
 * @returns {void}
125
 */
126
  startLifecycle(): void {
127 33
    setImmediate(() => {
128 33
      if (this.started) {
129 1
        return;
130
      }
131 32
      logger.debug('Starting lifecycle');
132 32
      process.on('beforeExit', this.beforeExit.bind(this));
133 32
      this.execution = this.execute();
134
    });
135
  }
136
137
  /**
138
 * Stops the lifecycle of the ExpressBeans application.
139
 * All tasks are cleared and the lifecycle is stopped.
140
 * USE ONLY IF YOU KNOW WHAT YOU ARE DOING
141
 * @returns {Promise<void>}
142
 */
143
  async stopLifecycle(): Promise<void> {
144 89
    logger.debug('Stopping lifecycle');
145 89
    await this.executePhase('exit').then(() => {
146 89
      this.phasePromises.clear();
147 89
      this.tasks.clear();
148 89
      this.started = false;
149 89
      this.execution = null;
150 89
      this.eventEmitter.removeAllListeners();
151 89
      this.eventEmitter = new EventEmitter<ExecutorEventMap>();
152 89
      logger.debug('Lifecycle stopped');
153 89
      process.removeListener('beforeExit', this.beforeExit.bind(this));
154
    });
155
  }
156
}
157
158
type ExecutorType = typeof ExecutorImpl & ExecutorImpl;
159 10
let instance: ExecutorImpl | null = null;
160
161
function getInstance() {
162 284
  instance ??= new ExecutorImpl();
163 284
  return instance;
164
}
165
166 10
export const Executor: ExecutorType = new Proxy(ExecutorImpl, {
167
  get(target, prop, receiver) {
168 285
    if (prop in target) {
169 1
      return Reflect.get(target, prop, receiver);
170
    }
171
172 284
    const inst = getInstance();
173 284
    const value = Reflect.get(inst, prop, inst);
174
175 284
    if (typeof value === 'function') {
176 253
      return value.bind(inst);
177
    }
178
179 31
    return value;
180
  },
181
}) as ExecutorType;
182